元件通訊 (component communication) 是元件架構裡面重要的一環,其中父元件向子元件提供輸入,子元件將結果傳回給其父元件。
我將示範 Angular 元件之間資料共享的四種模式。他們是:
export const routes: Route[] = [
{
path: 'input-output',
loadComponent: () => import('./communication/components/app-input-output.component'),
data: {
secretValue: 'my-secret',
}
},
{
path: 'signal-in-service',
loadComponent: () => import('./communication/components/app-signal-in-service.component'),
},
{
path: 'provide-inject',
loadComponent: () => import('./communication/components/app-provide-inject.component'),
},
{
path: 'signal-state',
loadComponent: () => import('./communication/components/app-signal-state.component'),
},
];
該應用程式有四個元件來演示資料共享模式。input-output
路由具有將被解析並成為 Angular 元件的signal input 的路由資料 (route data)。在 provideRouter
函數中啟用 withComponentInputBinding
功能後就可以實作。
import { ApplicationConfig, provideExperimentalZonelessChangeDetection } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideExperimentalZonelessChangeDetection(),
provideRouter(routes, withComponentInputBinding())
]
}
bootstrapApplication(App, appConfig);
appConfig
提供到 provideRouter
函數的路由,withComponentInputBinding
功能將secretValue
資料綁定到 signal input。 然後,bootstrapApplication
使用 App 元件和設定引導應用程式。
.enabled {
border: 1px solid black;
border-radius: 0.25rem;
}
// app-input-output.component.ts
@Component({
selector: 'app-input-output',
standalone: true,
imports: [AppInputOutputGrandchildComponent],
template: `
<h3>Input/Output Component</h3>
<div [class.enabled]="isEnabled()">
<app-input-output-grandchild [secretValue]="secretValue()" (featureFlag)="handleClicked($event)" />
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class AppInputOutputComponent {
secretValue = input.required<string>();
isEnabled = signal(false);
featureFlag = output<Feature>();
handleClicked(value: Feature) {
this.isEnabled.set(value.isShown);
this.featureFlag.emit(value);
}
}
AppInputOutputComponent
元件是一個父元件,它將資料綁定到 AppInputOutputGrandchildComponent
元件的 input,並將結果傳送到 App
元件。 AppInputOutputComponent
有一個 secretVaue
input,並傳遞給其子元件的 input。 它也聲明了一個發出功能名稱和狀態的 featureFlag
output 函數。 當 AppInputOutputGrandchildComponent
的自訂 featureFlag
事件發出新結果時,handleClicked
方法會更新 featureFlag
output 函數以將其傳播到 App
元件。此外,isEnabled
訊號 會切換 enabled
類別以在子元件周圍繪製邊框。
// grand-child.component.html
<h3>{{ title }}</h3>
<div>
<p>Secret Value: {{ secretValue() }}</p>
<p>{{ toggleText() }}</p>
<button (click)="handleClicked()">Click Me!!!!</button>
</div>
@Component({
selector: 'app-input-output-grandchild',
standalone: true,
templateUrl: './grand-child.component.html',
})
export default class AppInputOutputGrandchildComponent {
secretValue = input.required<string>();
toggleFeature = signal(false);
toggleText = computed(() => {
const click = this.toggleFeature() ? 'disable' : 'enable';
return `Click the button to ${click} the input/output feature`;
});
featureFlag = output<Feature>();
handleClicked() {
this.toggleFeature.set(!this.toggleFeature());
this.featureFlag.emit({
name: 'Input/Output feature',
isShown: this.toggleFeature()
});
}
}
AppInputOutputGrandchildComponent
是一個表現元件,它接收來自父元件的 input,並使用 RxJs-interop output 函數通知父元件 featureFlag
的狀態已變更。
signal input
和 output emitter
模式很簡單,但對於深度嵌套的元件樹來說並不是最佳的。這些元件容易出現 "input/output" 複製,其中每一層都必須複製相同的 input 和 output emitter。
因此,在 Signals in a Service,元件可以注入服務來存取或更新 訊號。
import { computed, Injectable, signal } from '@angular/core';
import { FeatureFacade } from '../feature.facade';
import { Feature } from '../types/feature.type';
@Injectable({
providedIn: 'root'
})
export class CommunicationService implements FeatureFacade {
#secretValue = signal('');
secretValue = this.#secretValue.asReadonly();
#feature = signal<Feature | null>(null);
feature = this.#feature.asReadonly();
featureName = computed(() => {
const feature = this.feature() || { name: '', isShown: false };
return feature.isShown ? feature.name : '';
});
setSecretValue(value: string): void {
this.#secretValue.set(value);
}
setFeature(feature: Feature | null): void {
this.#feature.set(feature);
}
}
CommunicationService
服務定義了 setScretValue
和 setFeature
方法來更新 Writable signals。 它也公開只讀訊號、secretValue、feature 和 featureName。
在 App
元件中,secretValue
在 ngOnInit
生命週期方法中設定。
ngOnInit(): void {
this.communicationService.setSecretValue('signal-in-a-service-secret');
this.signalStateService.setSecretValue('signal-state-secret');
}
// app-signal-in-service.component.ts
@Component({
selector: 'app-signal-in-service',
standalone: true,
imports: [AppSignalInServiceGrandchildComponent],
template: `
<h3>Signal in a Service Component</h3>
<div [class.enabled]="isEnabled()">
<app-signal-in-service-grandchild />
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class AppSignalInServiceComponent {
isEnabled = computed(() => {
const feature = this.communicationService.feature();
return feature?.isShown || false
});
communicationService = inject(CommunicationService);
}
父元件存取服務的唯讀訊號以建立 isEnabled
計算訊號 (computed signal)。isEnabled
何時應用 enabled
類別在子元件周圍繪製邊框。
// app-signal-in-service-grandchild.component.ts
@Component({
selector: 'app-signal-in-service-grandchild',
standalone: true,
templateUrl: './grand-child.component.html',
})
export default class AppSignalInServiceGrandchildComponent {
communicationService = inject(CommunicationService);
toggleFeature = signal(false);
secretValue = this.communicationService.secretValue;
toggleText = computed(() => /* same logic */);
handleClicked() {
this.toggleFeature.set(!this.toggleFeature());
this.communicationService.setFeature({
name: 'Signal in a Service feature',
isShown: this.toggleFeature()
});
}
}
AppSignalInServiceGrandchildComponent
元件呼叫服務的 setFeature
方法來更新功能標誌。它還從服務獲取 secretValue
訊號並在模板中顯示該值。
當元件狀態包含很少的資訊時,開發人員可能會認為 Signals in a Service 模式過於複雜。然後,他們可以將狀態封裝在一個物件中並將其註入到元件中,從而完全刪除服務。元件不注入服務,而是注入一個 injection token。
// provider-inject.type.ts
import { WritableSignal } from '@angular/core';
import { Feature } from './feature.type';
export type ProvideInjectToken = {
secretValue: WritableSignal<string>;
feature: WritableSignal<Feature | null>;
}
// provide-inject.constant.ts
import { InjectionToken } from '@angular/core';
import { ProvideInjectToken } from './types/provide-inject.type';
export const PROVIDE_INJECT_TOKEN = new InjectionToken<ProvideInjectToken>('PROVIDE_INJECT_TOKEN');
PROVIDE_INJECT_TOKEN
是注入 ProvideInjectToken
實例的 injection token。 ProvideInjectToken
類型由兩個訊號屬性組成:secretValue
和 feature
。然後元件可以讀取或更新註入物件的訊號屬性。
@Component({
selector: 'app-root',
providers: [
{
provide: PROVIDE_INJECT_TOKEN,
useValue: {
secretValue: signal(''),
feature: signal(null)
}
}
],
})
export class App implements OnInit {}
App
元件將 PROVIDE_INJECT_TOKEN
token 注入 'providers' array 中。因此,AppProvideInjectComponent
和 AppProvideInjectGrandchildComponent
元件可以注入 token來取得 ProvideInjectToken
的實例。這些元件可以讀取訊號 、顯示值或為其指派新值。
// app-provide-inject.component.ts
@Component({
selector: 'app-provide-inject-service',
standalone: true,
imports: [AppProvideInjectGrandchildComponent],
template: `
<h3>Provide/Inject Component</h3>
<div [class.enabled]="isEnabled()">
<app-provide-inject-grandchild />
</div>
`,
})
export default class AppProvideInjectComponent {
token = inject(PROVIDE_INJECT_TOKEN);
isEnabled = computed(() => this.token.feature()?.isShown || false);
}
父元件注入 PROVIDE_INJECT_TOKEN
並使用 feature
訊號來匯出 isEnabled
計算訊號 (computed signal)。當值為 true 時,enabled
類別會在子元件周圍新增邊框。否則,子組件周圍沒有邊框。
// app-provide-injector-grandchild.component.ts
@Component({
selector: 'app-provide-inject-grandchild',
standalone: true,
templateUrl: './grand-child.component.html',
})
export default class AppProvideInjectGrandchildComponent {
toggleFeature = signal(false);
token = inject(PROVIDE_INJECT_TOKEN);
secretValue = computed(() => this.token.secretValue());
toggleText = computed(() => { …same logic… });
handleClicked() {
this.toggleFeature.set(!this.toggleFeature());
this.token.feature.set({
name: 'Provide/Inject feature',
isShown: this.toggleFeature()
});
}
}
子元件宣告一個 secretValue
計算訊號以傳回 token 的 secretValue
訊號。在 handleClicked
方法中,token 的 feature
訊號被指派一個新值。
最後,我們嘗試了上述模式,應用程式已經發展到企業規模。現在是時候使用 state management library 來解決資料共享問題,而不是使用自訂的解決方案。
npm install @ngrx/signals
將 NgRx Signal State 安裝到專案中。
// signal-state.service.ts
@Injectable({
providedIn: 'root'
})
export class SignalStateService implements FeatureFacade {
#state = signalState<{ secretValue: string, feature: Feature | null }>({
secretValue: '',
feature: null,
});
secretValue = computed(() => this.#state.secretValue());
feature = computed(() => this.#state.feature());
featureName = computed(() => {
const feature = this.feature() || { name: '', isShown: false };
return feature.isShown ? feature.name : '';
});
setSecretValue(secretValue: string): void {
patchState(this.#state, () => ({ secretValue }));
}
setFeature(feature: Feature | null): void {
patchState(this.#state, () => ({ feature }))
}
}
#state
是由 secretValue
和 feature
屬性組成的訊號狀態 (signal state)。 setSecretValue
和 setFeature
方法分別修補狀態中的 secretValue
和 feature
的值。 secretValue
、feature
和 featureName
是從狀態中提取屬性的計算訊號 (computed signals)。
// app-signal-state.component.ts
@Component({
selector: 'app-signal-state',
standalone: true,
imports: [AppSignalStateGrandchildComponent],
template: `
<h3>NgRx Signal State Component</h3>
<div [class.enabled]="isEnabled()">
<app-signal-state-grandchild />
</div>
`,
})
export default class AppSignalStateComponent {
isEnabled = computed(() => {
const feature = this.service.feature();
return feature?.isShown || false
});
service = inject(SignalStateService);
}
類似地,父元件注入 SignalStateService
服務並使用 feature
訊號來衍生 isEnabled
計算訊號 (computed signal)。
// app-signal-state-grandchild.component.ts
@Component({
selector: 'app-signal-state-grandchild',
standalone: true,
templateUrl: './grand-child.component.html',
})
export default class AppSignalStateGrandchildComponent {
service = inject(SignalStateService);
toggleFeature = signal(false);
secretValue = this.service.secretValue;
toggleText = computed(() => { …same logic… });
handleClicked() {
this.toggleFeature.set(!this.toggleFeature());
this.service.setFeature({
name: 'Signal State feature',
isShown: this.toggleFeature()
});
}
}
子元件注入 SignalStateService
以顯示 secretValue
訊號的值。類似地,handleClicked
方法呼叫服務的 setFeature
方法來更新 feature
訊號。
// main.ts
@Component({
selector: 'app-root',
standalone: true,
imports: [NavbarComponent, RouterOutlet],
template: `
<p>Selected feature: {{ unifiedFeature() }}</p>
<router-outlet (activate)="onActivate($event)" (deactivate)="onDeactivate()" />
`,
providers: [...omitted for brevity reason…],
})
export class App implements OnInit {
feature = signal<string>('');
communicationService = inject(CommunicationService);
signalStateService = inject(SignalStateService);
token = inject(PROVIDE_INJECT_TOKEN);
unifiedFeature = computed(() => {
const tokenFeature = this.token.feature();
const tokenFeatureName = tokenFeature?.isShown ? tokenFeature.name : '';
return this.feature() || this.communicationService.featureName() || tokenFeatureName || this.signalStateService.featureName();
})
onActivate(component: any) {
if (component.featureFlag) {
component.featureFlag.subscribe((v: Feature) => {
this.feature.set(v.isShown ? v.name : '');
});
}
}
onDeactivate() {
this.token.feature.set(null);
this.communicationService.setFeature(null);
this.feature.set('');
this.signalStateService.setFeature(null);
}
}
RouterOutlet
的 activated
事件通知被路由的元件。 當元件為 AppInputOutputComponent
時,更新 feature
訊號。當不再對某個元件進行路由時,所有 feature
訊號都會重設為空。
unifiedFeature
是一個計算訊號 (computed signal),用於衍生功能的名稱。最多有一個 feature
訊號的 isShown 屬性等於 true。 此功能的名稱是 unifiedFeature
訊號的結果。
signal inputs
和 RxJS-interop output
在父元件和子元件之間共享資料。鐵人賽的第 36 天到此結束